package com;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * @Classname: NIOServer
 * @Author: bromide
 * @CreateTime: 2022/4/17--14:36
 * @Version: V1.0
 * @Description:
 * NIO实现聊天室服务端
 */
public class NIOServer {
    public static void main(String[] args) {
        try {
            //存放所有客户端的SocketChannel，用于广播消息
            List<SocketChannel> allChannel = new ArrayList<>();



            //创建总机，ServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //ServerSocketChannel默认为阻塞模式，可以将其设置为非阻塞模式
            serverSocketChannel.configureBlocking(false);//传入false设置为非阻塞模式
            //为ServerSocketChannel绑定服务端口，客户端可以通过该端口与我们建立连接
            serverSocketChannel.bind(new InetSocketAddress(8088));
            //以上创建为固定模式，以后都可以用这样的形式创建ServerSocketChannel的非阻塞模式

            /*
                多虑选择器的应用
                这个是NIO解决非阻塞的关键API，用于监听所有通道对应的事件，并做出对应的操作。
                我们的线程只要轮询处理多路选择器中待处理的通道时间即可完成所有的通道的工作，避免使用大量线程
                处于阻塞来减少不必要的系统开销。
             */
            Selector selector = Selector.open();//使用其静态方法open获取一个多路选择器实例

            /**
             * 让"总机"ServerSocketChannel向多路选择器上注册一个事件，即：accept事件。
             * 原因：原来使用ServerSocket时，一旦主线程调用accept方法就会进入阻塞状态，直到一个客户端连接
             * 否则将无法继续执行其他工作。而现在的操作时让多路选择器关心该事件，避免让线程处于阻塞。
             */
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
            /*
                多路选择器的select方法
                当注册在该选择器上的channel有事件待处理时，此方法会立即返回一个int值，表示有多少个事件待处理
                若没有任何事件待处理时，此方法会形成阻塞。
             */
                System.out.println("等待选择器告知是否有事件等待处理....");
                selector.select();
                System.out.println("选择器有事件待处理！！！！");
                //通过选择器获取所有带处理的事件
                Set<SelectionKey> keySet = selector.selectedKeys();
                //遍历集合，将所有待处理的事件处理完毕
                for (SelectionKey key : keySet) {
                    //判断该事件是否为可以接受一个客户端连接了(对应的时向多路选择器注册的事件：SelectionKey.OP_READ)
                    if (key.isAcceptable()) {
                        //处理接受客户端的连接操作
                    /*
                        通过SelectionKey的方法channel()获取该事件触发的通道
                        因为只有ServerSocketChannel向多路选择器注册了OP_ACCEPT事件，因此当该事件
                        isAcceptable()返回值为true，则说明该事件一定时由ServerSocketChannel触发的
                        所以我们通过该事件获取触发该事件的通道时，一定获取的是ServerSocketChannel
                     */
                        key.channel();
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    /*
                        获取SocketChannel与原来ServerSocket.accept返回的Socket道理一致
                        通过该SocketChannel就可以与连接的客户端进行双向交互数据了
                     */
                        SocketChannel socketChannel = channel.accept();//接受客户端的连接

                    /*
                        非阻塞的ServerSocketChannel就算多路选择器提示由客户端清酒可接受了，accept返回时
                        得到的SocketChannel有可能为null
                     */
                        if (socketChannel == null) {
                            continue;//忽略本次事件的后续处理
                        }
                    /*
                        当我们接受了客户端连接后，原来的聊天室服务端我们通过Socket获取输入流读取客户端
                        发送过来的消息的操作时如果客户端不发送内容，那么读取操作就会阻塞！
                        对此，我们将当前SocketChannel它的读取消息操作也注册到多路选择器中，这样一来只有
                        当客户端发送过来消息时，多路选择器才会通知我们进行处理。
                     */
                        //将当前SocketChannel设置为非阻塞模式
                        socketChannel.configureBlocking(false);
                        //向多路选择器中注册读取客户端消息的事件
                        socketChannel.register(selector, SelectionKey.OP_READ);
                        allChannel.add(socketChannel);
                        System.out.println("一个客户端连接了！当前在线人数：" + allChannel.size());
                    //该事件是否为某个SocketChannel有消息可读(某个客户端发送来了消息)
                    }else if (key.isReadable()){
                        try {
                            //通过事件获取触发了该事件的Channel
                            SocketChannel socketChannel = (SocketChannel) key.channel();
                            //通过SocketChannel读取该客户端发送过来的消息
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            //非阻塞状态下，有可能读取数据时未读取到任何字节
                            int len = socketChannel.read(buffer);
                            if (len==0){
                                continue;
                            }else if (len==-1){//若本次读取的长度返回值为-1则表示客户端断开连接了
                                socketChannel.close();//关闭SocketChannel与客户端也断开
                                allChannel.remove(socketChannel);
                                continue;
                            }
                            buffer.flip();//翻转后position到limit之间就是本次读取到的数据了
                            byte[] data = new byte[buffer.limit()];
                        /*
                            Buffer的get方法要求我们传入一个字节数组，作用时将当前Buffer中从下标
                            position开始的连续data数组长度的字节量装入该data数组。
                         */
                            buffer.get(data);//调用完毕后，data中保存的就是buffer中本次读取到的所有字节了
                            //将读取到的消息转换成字符串(客户端发送过来的消息)
                            String line = new String(data, StandardCharsets.UTF_8);
                            String host = socketChannel.socket().getInetAddress().getHostAddress();
                            System.out.print(host+"说："+line);

                            //遍历所有SocketChannel,将该消息发送给所有客户端
                            for (SocketChannel sc:allChannel){
                                buffer.flip();
                                sc.write(buffer);
                            }
                        }catch (IOException e){
                            //读取客户端消息这里若抛出异常，则通常是客户端强行断开连接造成的。
                            key.channel().close();
                            allChannel.remove(key.channel());
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
